package org.poptest.reflect;

import org.poptest.Callback;
import org.poptest.model.Predicate;
import org.poptest.util.AssertInternal;
import org.poptest.util.ClassUtils;
import org.poptest.util.CollectionUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Stack;

/**
 * @author Alan Roche
 *         Date: 26/01/11
 *         Time: 21:53
 */
public final class ReflectionUtils {

    /**
     * Instantiate via reflection. Converts checked xcps to unchecked
     *
     * @param clazz
     * @param args
     * @param <T>
     * @return
     */
    public static <T> T instantiate(Class<T> clazz, Object... args) {
        final T instance;
        try {
            final Constructor<T> constructor = clazz.getConstructor(ClassUtils.typesForArgs(args));
            instance = constructor.newInstance(args);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return instance;
    }

    /**
     * Call a method via reflection. Breaks access protection, ie. can call private/protected etc.
     *
     * @param target
     * @param methodName
     * @param args
     * @return
     */
    public static Object invoke(final Object target, final Class<?> targetClass, final String methodName, final Object... args) {
        final Object returnValue;
        try {
            final Method method = targetClass.getDeclaredMethod(methodName, getTypes(args));
            final boolean accessible = method.isAccessible();
            try {
                method.setAccessible(true);
                returnValue = method.invoke(target, args);
            } finally {
                method.setAccessible(accessible);
            }
        } catch (final Exception e) {
            throw new RuntimeException(e);
        }

        return returnValue;
    }

    @SuppressWarnings("unchecked")
    public static <RETURN_TYPE>
    RETURN_TYPE invoke(final Object target, final Class<?> targetClass, final String methodName, final Class<RETURN_TYPE> expectedReturnType, final Object... args) {
        final Object returnValue = invoke(target, targetClass, methodName, args);
        AssertInternal.isInstance(returnValue, expectedReturnType);
        return (RETURN_TYPE) returnValue;
    }

    public static Class<?>[] getTypes(final Object[] args) {
        final Class<?>[] types = new Class[args.length];
        for (int i = 0; i < args.length; i++) {
            types[i] = args[i].getClass();
        }
        return types;
    }

    public static Object getValue(final Object target, final Field field) {
        final Object returnValue;
        boolean accessible = field.isAccessible();
        if (!accessible) {
            field.setAccessible(true);
        }
        try {
            returnValue = field.get(target);
        } catch (IllegalAccessException e) {
            throw new RuntimeException(e);
        } finally {
            field.setAccessible(accessible);
        }
        return returnValue;
    }

    @SuppressWarnings("unchecked")
    public static <T> T getValue(final Object target, final Field field, final Class<T> expectedType) {
        final Object returnValue = getValue(target, field);
        AssertInternal.isInstance(returnValue, expectedType);
        return (T) returnValue;
    }

    /**
     * Returns a stach representing the class hierarcy
     * @return
     */
    public static Stack<Class<?>> hierarchy(final Class<?> target) {
        final Stack<Class<?>> hierarchyStack = new Stack<Class<?>>();
        Class<?> clazz = target;
        do {
            hierarchyStack.push(clazz);
            clazz = clazz.getSuperclass();
        } while (clazz != null);

        return  hierarchyStack;
    }

    /**
     * Applies the callback to all fields of the class.
     * Callback is applied in order of declaration, starting with the root baseclass and working down the hierarchy.
     * @param target
     * @param executable
     */
    public static void foreachField(final Object target, final MemberCallback<Field> executable) {

        final Stack<Class<?>> hierarchyStack = hierarchy(target.getClass());
        Class<?> clazz = hierarchyStack.pop();

        do {
            for (final Field field : clazz.getDeclaredFields()) {
                executable.execute(field);
            }
            if (!hierarchyStack.isEmpty()) {
                clazz = hierarchyStack.pop();
            }
        } while (clazz != null && !hierarchyStack.isEmpty());
    }


    public static <Object> boolean hasInstance(final Class<?> clazz, final Collection<Object> instances) {
        return CollectionUtils.findFirst(instances,  new Predicate<Object>() {
            public boolean isMatch(Object item) {
                return clazz.isInstance(item);
            }
        }) != null;
    }

    public static <Object> boolean hasInstance(final Class<?> clazz, final Object[] instances) {
        return hasInstance(clazz, Arrays.asList(instances));
    }
}
